import 'dart:typed_data';

import 'package:dbio_utils/tabix/util.dart';
import 'package:flutter/services.dart';

import '../tabix/chunk.dart';
import '../tabix/virtual_offset.dart';
import '../tabix/index_file.dart';
import 'util.dart';

import 'dart:math' show min, max;

var BAI_MAGIC = 21578050; // BAI\1

int roundDown(num n, num multiple) {
  return n - (n % multiple);
}

int roundUp(num n, num multiple) {
  return n - (n % multiple) + multiple;
}

class BAI extends IndexFile {
  Map parsePseudoBin(ByteData bytes, int offset) {
    int lineCount = bytes.getInt32(offset + 16, Endian.little);
    // var lineCount = longToNumber(Long.fromBytesLE(Array.prototype.slice.call(bytes, offset + 16, offset + 24), true));
    return {'lineCount': lineCount};
  }

  @override
  Future<List<Chunk>> blocksForRange({String refName, int start, int end, Map opts}) async {
    if (start < 0) {
      start = 0;
    }

    IndexBean indexData = await this.parse(opts);
    if (indexData == null) {
      return [];
    }
    var ba = indexData['indices'][refName];
    if (ba == null) {
      return [];
    }

    List<int> overlappingBins = this.reg2bins(start, end); // List of bin #s that overlap min, max
    List<Chunk> chunks = [];

    // Find chunks in overlapping bins.  Leaf bins (< 4681) are not pruned
    overlappingBins.forEach((bin) {
      if (ba['binIndex'][bin] != null) {
        var binChunks = ba['binIndex'][bin];
        for (var c = 0; c < binChunks.length; ++c) {
          chunks.add(Chunk(binChunks[c].minv, binChunks[c].maxv, bin));
        }
      }
    });

    // Use the linear index to find minimum file position of chunks that could contain alignments in the region
    int nintv = ba['linearIndex'].length;
    var lowest = null;
    var minLin = min(start >> 14, nintv - 1);
    var maxLin = min(end >> 14, nintv - 1);
    for (var i = minLin; i <= maxLin; ++i) {
      var vp = ba['linearIndex'][i];
      if (vp) {
        if (!lowest || vp.compareTo(lowest) < 0) {
          lowest = vp;
        }
      }
    }

    return optimizeChunks(chunks, lowest);
  }

  List<Chunk> optimizeChunks(List<Chunk> chunks, VirtualOffset lowest) {
    List<Chunk> mergedChunks = [];
    Chunk lastChunk;

    if (chunks.length == 0) {
      return chunks;
    }

    chunks.sort((c0, c1) {
      var dif = c0.minv.blockPosition - c1.minv.blockPosition;
      if (dif != 0) {
        return dif;
      } else {
        return c0.minv.dataPosition - c1.minv.dataPosition;
      }
    });

    chunks.forEach((chunk) {
      if (lowest != null || chunk.maxv.compareTo(lowest) > 0) {
        if (lastChunk == null) {
          mergedChunks.add(chunk);
          lastChunk = chunk;
        } else {
          if (canMergeBlocks(lastChunk, chunk)) {
            if (chunk.maxv.compareTo(lastChunk.maxv) > 0) {
              lastChunk.maxv = chunk.maxv;
            }
          } else {
            mergedChunks.add(chunk);
            lastChunk = chunk;
          }
        }
      }
      // else {
      //   console.log(`skipping chunk ${chunk}`)
      // }
    });

    return mergedChunks;
  }

  @override
  Future<int> lineCount(String refId, Map options) async {
    BaiBean prom = await this.parse(options);
    var index = prom.indices[refId] ?? {};
    if (index == null) {
      return -1;
    }
    Map ret = index['stats'] ?? {};
    return ret['lineCount'] ?? -1;
  }

  @override
  Future<BaiBean> parseImpl(Map option) async {
    var data = BaiBean({
      'bai': true,
      'maxBlockSize': 1 << 16,
    });

    Uint8List __bytes = await this.fileHandle.readFile();
    ByteData bytes = ByteData.view(__bytes.buffer);
    // check BAI magic numbers
    if (bytes.getUint32(0, Endian.little) != BAI_MAGIC) {
      throw new Exception('Not a BAI file');
    }

    data['refCount'] = bytes.getInt32(4, Endian.little);
    var depth = 5;
    var binLimit = ((1 << ((depth + 1) * 3)) - 1) / 7;

    // read the indexes for each reference sequence
    data['indices'] = List(data['refCount']);
    var currOffset = 8;
    for (var i = 0; i < data['refCount']; i += 1) {
      abortBreakPoint(option['signal']);

      // the binning index
      var binCount = bytes.getInt32(currOffset, Endian.little);
      var stats;

      currOffset += 4;
      Map<int, List<Chunk>> binIndex = {};
      for (var j = 0; j < binCount; j += 1) {
        var bin = bytes.getUint32(currOffset, Endian.little);
        currOffset += 4;
        if (bin == binLimit + 1) {
          currOffset += 4;
          stats = this.parsePseudoBin(bytes, currOffset);
          currOffset += 32;
        } else if (bin > binLimit + 1) {
          throw Exception('bai index contains too many bins, please use CSI');
        } else {
          var chunkCount = bytes.getInt32(currOffset, Endian.little);
          currOffset += 4;
          var chunks = List(chunkCount);
          for (var k = 0; k < chunkCount; k += 1) {
            var u = VirtualOffset.fromBytes(bytes: __bytes, offset: currOffset);
            var v = VirtualOffset.fromBytes(bytes: __bytes, offset: currOffset + 8);
            currOffset += 16;
            data['firstDataLine'] = this.findFirstData(null, u);
            chunks[k] = new Chunk(u, v, bin);
          }
          binIndex[bin] = chunks;
        }
      }

      var linearCount = bytes.getInt32(currOffset, Endian.little);
      currOffset += 4;
      // as we're going through the linear index, figure out
      // the smallest virtual offset in the indexes, which
      // tells us where the BAM header ends
      var linearIndex = List(linearCount);
      for (var k = 0; k < linearCount; k += 1) {
        linearIndex[k] = VirtualOffset.fromBytes(bytes: __bytes, offset: currOffset);
        currOffset += 8;
        data['firstDataLine'] = this.findFirstData(data['firstDataLine'], linearIndex[k]);
      }

      data.indices[i] = {'binIndex': binIndex, 'linearIndex': linearIndex, 'stats': stats};
    }

    return data;
  }

  indexCov(
    int seqId,
    num start,
    num end,
    Map opts,
  ) async {
    int v = 16384;
    var range = start != null;
    var indexData = await this.parse(opts);
    var seqIdx = indexData.indices[seqId];
    if (seqIdx == null) {
      return [];
    }

    List linearIndex = seqIdx['linearIndex'];
    Map stats = seqIdx['stats'];
    if ((linearIndex?.length ?? 0) == 0) {
      return [];
    }
    int e = end != null ? roundUp(end, v) : (linearIndex.length - 1) * v;
    int s = start != null ? roundDown(start, v) : 0;
    List depths;
    if (range) {
      depths = List((e - s) ~/ v);
    } else {
      depths = List(linearIndex.length - 1);
    }
    var totalSize = linearIndex[linearIndex.length - 1]['blockPosition'];
    if (e > (linearIndex.length - 1) * v) {
      throw Exception('query outside of range of linear index');
    }
    var currentPos = linearIndex[s ~/ v]['blockPosition'];
    for (int i = s ~/ v, j = 0; i < e / v; i++, j++) {
      depths[j] = {
        'score': linearIndex[i + 1]['blockPosition'] - currentPos,
        'start': i * v,
        'end': i * v + v,
      };
      currentPos = linearIndex[i + 1]['blockPosition'];
    }
    return depths.map((d) {
      return {...d, 'score': (d['score'] * stats['lineCount']) / totalSize};
    });
  }

  /// calculate the list of bins that may overlap with region [beg,end) (zero-based half-open)
  /// @returns {Array[number]}
  List<int> reg2bins(int beg, int end) {
    List list = [0];
    end -= 1;
    for (var k = 1 + (beg >> 26); k <= 1 + (end >> 26); k += 1) {
      list.add(k);
    }
    for (var k = 9 + (beg >> 23); k <= 9 + (end >> 23); k += 1) {
      list.add(k);
    }
    for (var k = 73 + (beg >> 20); k <= 73 + (end >> 20); k += 1) {
      list.add(k);
    }
    for (var k = 585 + (beg >> 17); k <= 585 + (end >> 17); k += 1) {
      list.add(k);
    }
    for (var k = 4681 + (beg >> 14); k <= 4681 + (end >> 14); k += 1) {
      list.add(k);
    }
    return list;
  }
}
